﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.Net;
using WcfContrib.Extensions.Behaviors;
using System.Threading;
using WcfContrib.Metadata;
using System.Net.Sockets;
using System.Web;

namespace WcfContrib.Helpers
{
    /// <summary>
    /// A helper class for dealing with service endpoints
    /// </summary>
    public static class EndpointHelper
    {
        const int OnlineTimeoutInMilliseconds = 1000 * 15;
        static readonly HttpStatusCode[] DefaultHttpAvailableResponses = new HttpStatusCode[] { HttpStatusCode.OK, HttpStatusCode.Accepted, HttpStatusCode.Found };

        /// <summary>
        /// Tries boosting the service endpoint by replacing the serializer behavior to increase the max items in object graph.
        /// </summary>
        /// <param name="endpoint">The service endpoint.</param>
        /// <returns>Whether or not the boosting had taken place.</returns>
        public static bool TryBoosting(this ServiceEndpoint endpoint)
        {
            foreach (OperationDescription operation in endpoint.Contract.Operations)
            {
                DataContractSerializerOperationBehavior dcsob = operation.Behaviors.Find<DataContractSerializerOperationBehavior>();

                if (dcsob != null)
                {
                    operation.Behaviors.Remove(dcsob);

                    operation.Behaviors.Add(new BoostedSerializerOperationBehavior(operation));
                }
            }

            return endpoint.Binding.TryBoosting();
        }

        /// <summary>
        /// Determines whether the specified service address is online.
        /// </summary>
        /// <param name="serviceAddress">The service address.</param>
        /// <returns>
        /// 	<c>true</c> if the specified service address is online; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsOnline(string serviceAddress)
        {
            return IsOnline(serviceAddress, null);
        }

        /// <summary>
        /// Determines whether the specified service address is online.
        /// </summary>
        /// <param name="serviceAddress">The service address.</param>
        /// <param name="metadataAddress">The metadata address.</param>
        /// <returns>
        /// 	<c>true</c> if the specified service address is online; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsOnline(string serviceAddress, string metadataAddress)
        {
            return IsOnline(serviceAddress, metadataAddress, OnlineTimeoutInMilliseconds);
        }

        /// <summary>
        /// Determines whether the specified service address is online.
        /// </summary>
        /// <param name="serviceAddress">The service address.</param>
        /// <param name="metadataAddress">The metadata address.</param>
        /// <param name="timeoutInMilliseconds">The timeout in milliseconds.</param>
        /// <returns>
        /// 	<c>true</c> if the specified service address is online; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsOnline(string serviceAddress, string metadataAddress, int timeoutInMilliseconds)
        {
            Uri serviceUri = string.IsNullOrEmpty(serviceAddress) ? null : new Uri(serviceAddress);
            Uri metaUri = string.IsNullOrEmpty(metadataAddress) ? null : new Uri(metadataAddress);

            return IsOnline(serviceUri, metaUri, timeoutInMilliseconds);
        }

        /// <summary>
        /// Determines whether the specified service address is online.
        /// </summary>
        /// <param name="serviceAddress">The service address.</param>
        /// <param name="metadataAddress">The metadata address.</param>
        /// <returns>
        /// 	<c>true</c> if the specified service address is online; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsOnline(Uri serviceAddress, Uri metadataAddress)
        {
            return IsOnline(serviceAddress, metadataAddress, OnlineTimeoutInMilliseconds);
        }

        /// <summary>
        /// Determines whether the specified service address is online.
        /// </summary>
        /// <param name="serviceAddress">The service address.</param>
        /// <param name="metadataAddress">The metadata address.</param>
        /// <param name="timeoutInMilliseconds">The timeout in milliseconds.</param>
        /// <returns>
        /// 	<c>true</c> if the specified service address is online; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsOnline(Uri serviceAddress, Uri metadataAddress, int timeoutInMilliseconds)
        {
            return IsOnline(serviceAddress, metadataAddress, timeoutInMilliseconds, CredentialCache.DefaultCredentials);
        }

        /// <summary>
        /// Determines whether the specified service address is online.
        /// </summary>
        /// <param name="serviceAddress">The service address.</param>
        /// <param name="metadataAddress">The metadata address.</param>
        /// <param name="timeoutInMilliseconds">The timeout in milliseconds.</param>
        /// <param name="credentials">The credentials.</param>
        /// <returns>
        /// 	<c>true</c> if the specified service address is online; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsOnline(Uri serviceAddress, Uri metadataAddress, int timeoutInMilliseconds, ICredentials credentials)
        {
            bool hasService = serviceAddress != null;
            bool hasMeta = metadataAddress != null;

            if (hasMeta || hasService)
            {
                bool online = true;

                bool checkMeta = hasMeta;
                bool checkService = hasService && (!hasMeta || serviceAddress.AbsoluteUri != metadataAddress.AbsoluteUri);
                bool checkServiceAsMeta = checkService && (serviceAddress.LocalPath.Contains(".asmx") || serviceAddress.LocalPath.Contains(".svc"));

                if (checkService && checkMeta)
                {
                    Func<Uri, bool, int, ICredentials, bool> serviceCheck = new Func<Uri, bool, int, ICredentials, bool>(IsServiceOnline);
                    IAsyncResult serviceResult = serviceCheck.BeginInvoke(serviceAddress, checkServiceAsMeta, timeoutInMilliseconds, credentials, null, null);

                    Func<string, int, ICredentials, bool> metaCheck = new Func<string, int, ICredentials, bool>(IsMetadataAvailable);
                    IAsyncResult metaResult = metaCheck.BeginInvoke(metadataAddress.AbsoluteUri, timeoutInMilliseconds, credentials, null, null);

                    WaitHandle[] waitHandles = new WaitHandle[]
                    {
                        serviceResult.AsyncWaitHandle,
                        metaResult.AsyncWaitHandle
                    };

                    int index = WaitHandle.WaitAny(waitHandles);
                    bool serviceCompleted = (index == 0);

                    online = serviceCompleted
                        ? serviceCheck.EndInvoke(serviceResult)
                        : metaCheck.EndInvoke(metaResult);

                    if (online)
                    {
                        if (serviceCompleted)
                        {
                            online &= metaCheck.EndInvoke(metaResult);
                        }
                        else
                        {
                            online &= serviceCheck.EndInvoke(serviceResult);
                        }
                    }
                }
                else if (checkService)
                {
                    online = IsServiceOnline(serviceAddress, checkServiceAsMeta, timeoutInMilliseconds, credentials);
                }
                else if (checkMeta)
                {
                    online &= IsMetadataAvailable(metadataAddress.AbsoluteUri, timeoutInMilliseconds, credentials);
                }

                return online;
            }

            return false;
        }

        static bool IsServiceOnline(Uri serviceAddress, bool checkServiceAsMetadataAddress, int timeoutInMilliseconds, ICredentials credentials)
        {
            if (checkServiceAsMetadataAddress)
            {
                return IsMetadataAvailable(serviceAddress.AbsoluteUri, timeoutInMilliseconds, credentials);
            }
            else
            {
                return HasTcpConnnection(serviceAddress, timeoutInMilliseconds);
            }
        }

        static bool IsMetadataAvailable(string address, int timeoutInMilliseconds, ICredentials credentials)
        {
            return MetadataHelper.IsMetadataAvailable(address, credentials, timeoutInMilliseconds);
        }

        static bool HasTcpConnnection(Uri address, int timeoutInMilliseconds)
        {
            try
            {
                ManualResetEvent asyncWait = new ManualResetEvent(false);
                TcpClient tcpClient = new TcpClient();
                bool isConnected = false;

                tcpClient.BeginConnect(address.Host, address.Port,
                    state =>
                    {
                        try
                        {
                            tcpClient.EndConnect(state);

                            isConnected = tcpClient.Connected;

                            tcpClient.Close();
                        }
                        catch (SocketException) { }
                        finally
                        {
                            try
                            {
                                asyncWait.Set();
                            }
                            catch (ObjectDisposedException) { }
                        }
                    }, null);

                asyncWait.WaitOne(timeoutInMilliseconds);
                asyncWait.Close();

                return isConnected;
            }
            catch (SocketException)
            {
                return false;
            }
        }

        /// <summary>
        /// Determines whether the HTTP address is available.
        /// </summary>
        /// <param name="address">The HTTP address.</param>
        /// <param name="availableResponses">The http status responses to be considered as available.</param>
        /// <returns>
        /// 	<c>true</c> if the HTTP address is available; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsHttpAddressAvailable(Uri address, params HttpStatusCode[] availableResponses)
        {
            return IsHttpAddressAvailable(address, null, availableResponses);
        }

        /// <summary>
        /// Determines whether the HTTP address is available.
        /// </summary>
        /// <param name="address">The HTTP address.</param>
        /// <param name="credentials">The credentials.</param>
        /// <param name="availableResponses">The http status responses to be considered as available.</param>
        /// <returns>
        /// 	<c>true</c> if the HTTP address is available; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsHttpAddressAvailable(Uri address, ICredentials credentials, params HttpStatusCode[] availableResponses)
        {
            return IsHttpAddressAvailable(address, OnlineTimeoutInMilliseconds, credentials, availableResponses);
        }

        /// <summary>
        /// Determines whether the HTTP address is available.
        /// </summary>
        /// <param name="address">The HTTP address.</param>
        /// <param name="timeoutInMilliseconds">The timeout in milliseconds.</param>
        /// <param name="availableResponses">The http status responses to be considered as available.</param>
        /// <returns>
        /// 	<c>true</c> if the HTTP address is available; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsHttpAddressAvailable(Uri address, int timeoutInMilliseconds, params HttpStatusCode[] availableResponses)
        {
            return IsHttpAddressAvailable(address, timeoutInMilliseconds, CredentialCache.DefaultCredentials, availableResponses);
        }

        /// <summary>
        /// Determines whether the HTTP address is available.
        /// </summary>
        /// <param name="address">The HTTP address.</param>
        /// <param name="timeoutInMilliseconds">The timeout in milliseconds.</param>
        /// <param name="credentials">The credentials.</param>
        /// <param name="availableResponses">The http status responses to be considered as available.</param>
        /// <returns>
        /// 	<c>true</c> if the HTTP address is available; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsHttpAddressAvailable(Uri address, int timeoutInMilliseconds, ICredentials credentials, params HttpStatusCode[] availableResponses)
        {
            bool result = false;

            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(address);

            request.Timeout = timeoutInMilliseconds;
            request.Credentials = credentials;

            bool hasCustomResponses = availableResponses != null && availableResponses.Length > 0;

            try
            {
                using (WebResponse response = request.GetResponse())
                {
                    result = true;
                }
            }
            catch (WebException ex)
            {
                HttpWebResponse resp = ex.Response as HttpWebResponse;

                if (resp != null && (DefaultHttpAvailableResponses.Contains(resp.StatusCode) || (hasCustomResponses && availableResponses.Contains(resp.StatusCode))))
                {
                    result = true;
                }
            }
            catch (HttpException ex)
            {
                if (DefaultHttpAvailableResponses.Any(s => Convert.ToInt32(s) == ex.ErrorCode) || (hasCustomResponses && availableResponses.Any(s => Convert.ToInt32(s) == ex.ErrorCode)))
                {
                    result = true;
                }
            }

            return result;
        }

        internal static string CombineAddresses(string first, string second)
        {
            if (!first.EndsWith("/"))
            {
                first += "/";
            }
            if (second.StartsWith("/"))
            {
                second = second.Remove(0, 1);
            }

            return first + second;
        }
    }
}
